| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com> |
| ** Copyright (C) 2019 Menlo Systems GmbH, author Arno Rehn <a.rehn@menlosystems.com> |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtWebChannel module 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 "tst_webchannel.h" |
| |
| #include <qwebchannel.h> |
| #include <qwebchannel_p.h> |
| #include <qmetaobjectpublisher_p.h> |
| |
| #include <QtTest> |
| #ifdef WEBCHANNEL_TESTS_CAN_USE_JS_ENGINE |
| #include <QJSEngine> |
| #endif |
| |
| QT_USE_NAMESPACE |
| |
| #ifdef WEBCHANNEL_TESTS_CAN_USE_JS_ENGINE |
| class TestJSEngine; |
| |
| class TestEngineTransport : public QWebChannelAbstractTransport |
| { |
| Q_OBJECT |
| public: |
| TestEngineTransport(TestJSEngine *); |
| void sendMessage(const QJsonObject &message) override; |
| |
| Q_INVOKABLE void channelSetupReady(); |
| Q_INVOKABLE void send(const QByteArray &message); |
| private: |
| TestJSEngine *m_testEngine; |
| }; |
| |
| class ConsoleLogger : public QObject |
| { |
| Q_OBJECT |
| public: |
| ConsoleLogger(QObject *parent = 0); |
| |
| Q_INVOKABLE void log(const QString &text); |
| Q_INVOKABLE void error(const QString &text); |
| |
| int errorCount() const { return m_errCount; } |
| int logCount() const { return m_logCount; } |
| QString lastError() const { return m_lastError; } |
| |
| private: |
| int m_errCount; |
| int m_logCount; |
| QString m_lastError; |
| |
| }; |
| |
| |
| |
| ConsoleLogger::ConsoleLogger(QObject *parent) |
| : QObject(parent) |
| , m_errCount(0) |
| , m_logCount(0) |
| { |
| } |
| |
| void ConsoleLogger::log(const QString &text) |
| { |
| m_logCount++; |
| qDebug("LOG: %s", qPrintable(text)); |
| } |
| |
| void ConsoleLogger::error(const QString &text) |
| { |
| m_errCount++; |
| m_lastError = text; |
| qWarning("ERROR: %s", qPrintable(text)); |
| } |
| |
| |
| // A test JS engine with convenience integration with WebChannel. |
| class TestJSEngine : public QJSEngine |
| { |
| Q_OBJECT |
| public: |
| TestJSEngine(); |
| |
| TestEngineTransport *transport() const; |
| ConsoleLogger *logger() const; |
| void initWebChannelJS(); |
| |
| signals: |
| void channelSetupReady(TestEngineTransport *transport); |
| |
| private: |
| TestEngineTransport *m_transport; |
| ConsoleLogger *m_logger; |
| }; |
| |
| TestEngineTransport::TestEngineTransport(TestJSEngine *engine) |
| : QWebChannelAbstractTransport(engine) |
| , m_testEngine(engine) |
| { |
| } |
| |
| void TestEngineTransport::sendMessage(const QJsonObject &message) |
| { |
| QByteArray json = QJsonDocument(message).toJson(QJsonDocument::Compact); |
| QJSValue callback = m_testEngine->evaluate(QStringLiteral("transport.onmessage")); |
| Q_ASSERT(callback.isCallable()); |
| QJSValue arg = m_testEngine->newObject(); |
| QJSValue data = m_testEngine->evaluate(QString::fromLatin1("JSON.parse('%1');").arg(QString::fromUtf8(json))); |
| Q_ASSERT(!data.isError()); |
| arg.setProperty(QStringLiteral("data"), data); |
| QJSValue val = callback.call((QJSValueList() << arg)); |
| Q_ASSERT(!val.isError()); |
| } |
| |
| void TestEngineTransport::channelSetupReady() |
| { |
| emit m_testEngine->channelSetupReady(m_testEngine->transport()); |
| } |
| |
| void TestEngineTransport::send(const QByteArray &message) |
| { |
| QJsonDocument doc(QJsonDocument::fromJson(message)); |
| emit messageReceived(doc.object(), this); |
| } |
| |
| |
| TestJSEngine::TestJSEngine() |
| : m_transport(new TestEngineTransport(this)) |
| , m_logger(new ConsoleLogger(this)) |
| { |
| globalObject().setProperty("transport", newQObject(m_transport)); |
| globalObject().setProperty("console", newQObject(m_logger)); |
| |
| QString webChannelJSPath(QStringLiteral(":/qtwebchannel/qwebchannel.js")); |
| QFile webChannelJS(webChannelJSPath); |
| if (!webChannelJS.open(QFile::ReadOnly)) |
| qFatal("Error opening qwebchannel.js"); |
| QString source(QString::fromUtf8(webChannelJS.readAll())); |
| evaluate(source, webChannelJSPath); |
| } |
| |
| TestEngineTransport *TestJSEngine::transport() const |
| { |
| return m_transport; |
| } |
| |
| ConsoleLogger *TestJSEngine::logger() const |
| { |
| return m_logger; |
| } |
| |
| void TestJSEngine::initWebChannelJS() |
| { |
| globalObject().setProperty(QStringLiteral("channel"), newObject()); |
| QJSValue channel = evaluate(QStringLiteral("channel = new QWebChannel(transport, function(channel) { transport.channelSetupReady();});")); |
| Q_ASSERT(!channel.isError()); |
| } |
| |
| #endif // WEBCHANNEL_TESTS_CAN_USE_JS_ENGINE |
| |
| |
| TestWebChannel::TestWebChannel(QObject *parent) |
| : QObject(parent) |
| , m_dummyTransport(new DummyTransport(this)) |
| , m_lastInt(0) |
| , m_lastBool(false) |
| , m_lastDouble(0) |
| { |
| } |
| |
| TestWebChannel::~TestWebChannel() |
| { |
| |
| } |
| |
| int TestWebChannel::readInt() const |
| { |
| return m_lastInt; |
| } |
| |
| void TestWebChannel::setInt(int i) |
| { |
| m_lastInt = i; |
| emit lastIntChanged(); |
| } |
| |
| bool TestWebChannel::readBool() const |
| { |
| return m_lastBool; |
| } |
| |
| void TestWebChannel::setBool(bool b) |
| { |
| m_lastBool = b; |
| emit lastBoolChanged(); |
| } |
| |
| double TestWebChannel::readDouble() const |
| { |
| return m_lastDouble; |
| } |
| |
| void TestWebChannel::setDouble(double d) |
| { |
| m_lastDouble = d; |
| emit lastDoubleChanged(); |
| } |
| |
| QVariant TestWebChannel::readVariant() const |
| { |
| return m_lastVariant; |
| } |
| |
| void TestWebChannel::setVariant(const QVariant &v) |
| { |
| m_lastVariant = v; |
| emit lastVariantChanged(); |
| } |
| |
| QJsonValue TestWebChannel::readJsonValue() const |
| { |
| return m_lastJsonValue; |
| } |
| |
| void TestWebChannel::setJsonValue(const QJsonValue& v) |
| { |
| m_lastJsonValue = v; |
| emit lastJsonValueChanged(); |
| } |
| |
| QJsonObject TestWebChannel::readJsonObject() const |
| { |
| return m_lastJsonObject; |
| } |
| |
| void TestWebChannel::setJsonObject(const QJsonObject& v) |
| { |
| m_lastJsonObject = v; |
| emit lastJsonObjectChanged(); |
| } |
| |
| QJsonArray TestWebChannel::readJsonArray() const |
| { |
| return m_lastJsonArray; |
| } |
| |
| void TestWebChannel::setJsonArray(const QJsonArray& v) |
| { |
| m_lastJsonArray = v; |
| emit lastJsonArrayChanged(); |
| } |
| |
| int TestWebChannel::readOverload(int i) |
| { |
| return i + 1; |
| } |
| |
| QString TestWebChannel::readOverload(const QString &arg) |
| { |
| return arg.toUpper(); |
| } |
| |
| QString TestWebChannel::readOverload(const QString &arg, int i) |
| { |
| return arg.toUpper() + QString::number(i + 1); |
| } |
| |
| void TestWebChannel::testRegisterObjects() |
| { |
| QWebChannel channel; |
| QObject plain; |
| |
| QHash<QString, QObject*> objects; |
| objects[QStringLiteral("plain")] = &plain; |
| objects[QStringLiteral("channel")] = &channel; |
| objects[QStringLiteral("publisher")] = channel.d_func()->publisher; |
| objects[QStringLiteral("test")] = this; |
| |
| channel.registerObjects(objects); |
| } |
| |
| void TestWebChannel::testDeregisterObjects() |
| { |
| QWebChannel channel; |
| TestObject testObject; |
| testObject.setObjectName("myTestObject"); |
| |
| |
| channel.registerObject(testObject.objectName(), &testObject); |
| |
| channel.connectTo(m_dummyTransport); |
| channel.d_func()->publisher->initializeClient(m_dummyTransport); |
| |
| QJsonObject connectMessage = |
| QJsonDocument::fromJson(("{\"type\": 7," |
| "\"object\": \"myTestObject\"," |
| "\"signal\": " + QString::number(testObject.metaObject()->indexOfSignal("sig1()")) |
| + "}").toLatin1()).object(); |
| channel.d_func()->publisher->handleMessage(connectMessage, m_dummyTransport); |
| |
| emit testObject.sig1(); |
| channel.deregisterObject(&testObject); |
| emit testObject.sig1(); |
| } |
| |
| void TestWebChannel::testDeregisterObjectAtStart() |
| { |
| QWebChannel channel; |
| QVERIFY(channel.registeredObjects().isEmpty()); |
| |
| TestObject testObject; |
| testObject.setObjectName("myTestObject"); |
| |
| channel.registerObject(testObject.objectName(), &testObject); |
| QCOMPARE(channel.registeredObjects().size(), 1); |
| |
| channel.deregisterObject(&testObject); |
| QVERIFY(channel.registeredObjects().isEmpty()); |
| } |
| |
| void TestWebChannel::testInfoForObject() |
| { |
| TestObject obj; |
| obj.setObjectName("myTestObject"); |
| |
| QWebChannel channel; |
| const QJsonObject info = channel.d_func()->publisher->classInfoForObject(&obj, m_dummyTransport); |
| |
| QCOMPARE(info.keys(), QStringList() << "enums" << "methods" << "properties" << "signals"); |
| |
| { // enums |
| QJsonObject fooEnum; |
| fooEnum["Asdf"] = TestObject::Asdf; |
| fooEnum["Bar"] = TestObject::Bar; |
| QJsonObject testFlags; |
| testFlags["FirstFlag"] = static_cast<int>(TestObject::FirstFlag); |
| testFlags["SecondFlag"] = static_cast<int>(TestObject::SecondFlag); |
| QJsonObject expected; |
| expected["Foo"] = fooEnum; |
| expected["TestFlags"] = testFlags; |
| QCOMPARE(info["enums"].toObject(), expected); |
| } |
| |
| QJsonArray expected; |
| auto addMethod = [&expected, &obj](const QString &name, const char *signature, bool addName = true) { |
| const auto index = obj.metaObject()->indexOfMethod(signature); |
| QVERIFY2(index != -1, signature); |
| if (addName) |
| expected.append(QJsonArray{name, index}); |
| expected.append(QJsonArray{QString::fromUtf8(signature), index}); |
| }; |
| { // methods & slots |
| expected = {}; |
| addMethod(QStringLiteral("deleteLater"), "deleteLater()"); |
| addMethod(QStringLiteral("slot1"), "slot1()"); |
| addMethod(QStringLiteral("slot2"), "slot2(QString)"); |
| addMethod(QStringLiteral("setReturnedObject"), "setReturnedObject(TestObject*)"); |
| addMethod(QStringLiteral("setObjectProperty"), "setObjectProperty(QObject*)"); |
| addMethod(QStringLiteral("setProp"), "setProp(QString)"); |
| addMethod(QStringLiteral("fire"), "fire()"); |
| addMethod(QStringLiteral("overload"), "overload(double)"); |
| addMethod(QStringLiteral("overload"), "overload(int)", false); |
| addMethod(QStringLiteral("overload"), "overload(QObject*)", false); |
| addMethod(QStringLiteral("overload"), "overload(QString)", false); |
| addMethod(QStringLiteral("overload"), "overload(QString,int)", false); |
| addMethod(QStringLiteral("overload"), "overload(QJsonArray)", false); |
| addMethod(QStringLiteral("method1"), "method1()"); |
| QCOMPARE(info["methods"].toArray(), expected); |
| } |
| |
| { // signals |
| expected = {}; |
| addMethod(QStringLiteral("destroyed"), "destroyed(QObject*)"); |
| addMethod(QStringLiteral("destroyed"), "destroyed()", false); |
| addMethod(QStringLiteral("sig1"), "sig1()"); |
| addMethod(QStringLiteral("sig2"), "sig2(QString)"); |
| addMethod(QStringLiteral("replay"), "replay()"); |
| addMethod(QStringLiteral("overloadSignal"), "overloadSignal(int)"); |
| addMethod(QStringLiteral("overloadSignal"), "overloadSignal(float)", false); |
| QCOMPARE(info["signals"].toArray(), expected); |
| } |
| |
| { // properties |
| QJsonArray expected; |
| { |
| QJsonArray property; |
| property.append(obj.metaObject()->indexOfProperty("objectName")); |
| property.append(QStringLiteral("objectName")); |
| { |
| QJsonArray signal; |
| signal.append(1); |
| signal.append(obj.metaObject()->indexOfMethod("objectNameChanged(QString)")); |
| property.append(signal); |
| } |
| property.append(obj.objectName()); |
| expected.append(property); |
| } |
| { |
| QJsonArray property; |
| property.append(obj.metaObject()->indexOfProperty("foo")); |
| property.append(QStringLiteral("foo")); |
| { |
| QJsonArray signal; |
| property.append(signal); |
| } |
| property.append(obj.foo()); |
| expected.append(property); |
| } |
| { |
| QJsonArray property; |
| property.append(obj.metaObject()->indexOfProperty("asdf")); |
| property.append(QStringLiteral("asdf")); |
| { |
| QJsonArray signal; |
| signal.append(1); |
| signal.append(obj.metaObject()->indexOfMethod("asdfChanged()")); |
| property.append(signal); |
| } |
| property.append(obj.asdf()); |
| expected.append(property); |
| } |
| { |
| QJsonArray property; |
| property.append(obj.metaObject()->indexOfProperty("bar")); |
| property.append(QStringLiteral("bar")); |
| { |
| QJsonArray signal; |
| signal.append(QStringLiteral("theBarHasChanged")); |
| signal.append(obj.metaObject()->indexOfMethod("theBarHasChanged()")); |
| property.append(signal); |
| } |
| property.append(obj.bar()); |
| expected.append(property); |
| } |
| { |
| QJsonArray property; |
| property.append(obj.metaObject()->indexOfProperty("objectProperty")); |
| property.append(QStringLiteral("objectProperty")); |
| { |
| QJsonArray signal; |
| signal.append(1); |
| signal.append(obj.metaObject()->indexOfMethod("objectPropertyChanged()")); |
| property.append(signal); |
| } |
| property.append(QJsonValue::fromVariant(QVariant::fromValue(obj.objectProperty()))); |
| expected.append(property); |
| } |
| { |
| QJsonArray property; |
| property.append(obj.metaObject()->indexOfProperty("returnedObject")); |
| property.append(QStringLiteral("returnedObject")); |
| { |
| QJsonArray signal; |
| signal.append(1); |
| signal.append(obj.metaObject()->indexOfMethod("returnedObjectChanged()")); |
| property.append(signal); |
| } |
| property.append(QJsonValue::fromVariant(QVariant::fromValue(obj.returnedObject()))); |
| expected.append(property); |
| } |
| { |
| QJsonArray property; |
| property.append(obj.metaObject()->indexOfProperty("prop")); |
| property.append(QStringLiteral("prop")); |
| { |
| QJsonArray signal; |
| signal.append(1); |
| signal.append(obj.metaObject()->indexOfMethod("propChanged(QString)")); |
| property.append(signal); |
| } |
| property.append(QJsonValue::fromVariant(QVariant::fromValue(obj.prop()))); |
| expected.append(property); |
| } |
| QCOMPARE(info["properties"].toArray(), expected); |
| } |
| } |
| |
| void TestWebChannel::testInvokeMethodConversion() |
| { |
| QWebChannel channel; |
| channel.connectTo(m_dummyTransport); |
| |
| QJsonArray args; |
| args.append(QJsonValue(1000)); |
| |
| { |
| channel.d_func()->publisher->invokeMethod(this, "setInt", args); |
| QCOMPARE(m_lastInt, args.at(0).toInt()); |
| int getterMethod = metaObject()->indexOfMethod("readInt()"); |
| QVERIFY(getterMethod != -1); |
| auto retVal = channel.d_func()->publisher->invokeMethod(this, getterMethod, {}); |
| QCOMPARE(retVal, args.at(0).toVariant()); |
| } |
| { |
| QJsonArray args; |
| args.append(QJsonValue(!m_lastBool)); |
| channel.d_func()->publisher->invokeMethod(this, "setBool", args); |
| QCOMPARE(m_lastBool, args.at(0).toBool()); |
| int getterMethod = metaObject()->indexOfMethod("readBool()"); |
| QVERIFY(getterMethod != -1); |
| auto retVal = channel.d_func()->publisher->invokeMethod(this, getterMethod, {}); |
| QCOMPARE(retVal, args.at(0).toVariant()); |
| } |
| { |
| channel.d_func()->publisher->invokeMethod(this, "setDouble", args); |
| QCOMPARE(m_lastDouble, args.at(0).toDouble()); |
| int getterMethod = metaObject()->indexOfMethod("readDouble()"); |
| QVERIFY(getterMethod != -1); |
| auto retVal = channel.d_func()->publisher->invokeMethod(this, getterMethod, {}); |
| QCOMPARE(retVal, args.at(0).toVariant()); |
| } |
| { |
| channel.d_func()->publisher->invokeMethod(this, "setVariant", args); |
| QCOMPARE(m_lastVariant, args.at(0).toVariant()); |
| int getterMethod = metaObject()->indexOfMethod("readVariant()"); |
| QVERIFY(getterMethod != -1); |
| auto retVal = channel.d_func()->publisher->invokeMethod(this, getterMethod, {}); |
| QCOMPARE(retVal, args.at(0).toVariant()); |
| } |
| { |
| channel.d_func()->publisher->invokeMethod(this, "setJsonValue", args); |
| QCOMPARE(m_lastJsonValue, args.at(0)); |
| int getterMethod = metaObject()->indexOfMethod("readJsonValue()"); |
| QVERIFY(getterMethod != -1); |
| auto retVal = channel.d_func()->publisher->invokeMethod(this, getterMethod, {}); |
| QCOMPARE(retVal, args.at(0).toVariant()); |
| } |
| { |
| QJsonObject object; |
| object["foo"] = QJsonValue(123); |
| object["bar"] = QJsonValue(4.2); |
| args[0] = object; |
| channel.d_func()->publisher->invokeMethod(this, "setJsonObject", args); |
| QCOMPARE(m_lastJsonObject, object); |
| int getterMethod = metaObject()->indexOfMethod("readJsonObject()"); |
| QVERIFY(getterMethod != -1); |
| auto retVal = channel.d_func()->publisher->invokeMethod(this, getterMethod, {}); |
| QCOMPARE(retVal, QVariant::fromValue(object)); |
| } |
| { |
| QJsonArray array; |
| array << QJsonValue(123); |
| array << QJsonValue(4.2); |
| args[0] = array; |
| channel.d_func()->publisher->invokeMethod(this, "setJsonArray", args); |
| QCOMPARE(m_lastJsonArray, array); |
| int getterMethod = metaObject()->indexOfMethod("readJsonArray()"); |
| QVERIFY(getterMethod != -1); |
| auto retVal = channel.d_func()->publisher->invokeMethod(this, getterMethod, {}); |
| QCOMPARE(retVal, QVariant::fromValue(array)); |
| } |
| } |
| |
| void TestWebChannel::testFunctionOverloading() |
| { |
| QWebChannel channel; |
| channel.connectTo(m_dummyTransport); |
| |
| // all method calls will use the first method's index |
| const auto method1 = metaObject()->indexOfMethod("readOverload(int)"); |
| QVERIFY(method1 != -1); |
| const auto method2 = metaObject()->indexOfMethod("readOverload(QString)"); |
| QVERIFY(method2 != -1); |
| QVERIFY(method1 < method2); |
| const auto method3 = metaObject()->indexOfMethod("readOverload(QString,int)"); |
| QVERIFY(method3 != -1); |
| QVERIFY(method2 < method3); |
| |
| { // int |
| const auto retVal = channel.d_func()->publisher->invokeMethod(this, method1, QJsonArray{1000}); |
| QCOMPARE(retVal.toInt(), 1001); |
| } |
| { // QString |
| const auto retVal = channel.d_func()->publisher->invokeMethod(this, method2, QJsonArray{QStringLiteral("hello world")}); |
| QCOMPARE(retVal.toString(), QStringLiteral("HELLO WORLD")); |
| } |
| { // QString, int |
| const auto retVal = channel.d_func()->publisher->invokeMethod(this, method3, QJsonArray{QStringLiteral("the answer is "), 41}); |
| QCOMPARE(retVal.toString(), QStringLiteral("THE ANSWER IS 42")); |
| } |
| } |
| |
| void TestWebChannel::testSetPropertyConversion() |
| { |
| QWebChannel channel; |
| channel.connectTo(m_dummyTransport); |
| |
| { |
| int property = metaObject()->indexOfProperty("lastInt"); |
| QVERIFY(property != -1); |
| channel.d_func()->publisher->setProperty(this, property, QJsonValue(42)); |
| QCOMPARE(m_lastInt, 42); |
| } |
| { |
| int property = metaObject()->indexOfProperty("lastBool"); |
| QVERIFY(property != -1); |
| bool newValue = !m_lastBool; |
| channel.d_func()->publisher->setProperty(this, property, QJsonValue(newValue)); |
| QCOMPARE(m_lastBool, newValue); |
| } |
| { |
| int property = metaObject()->indexOfProperty("lastDouble"); |
| QVERIFY(property != -1); |
| channel.d_func()->publisher->setProperty(this, property, QJsonValue(-4.2)); |
| QCOMPARE(m_lastDouble, -4.2); |
| } |
| { |
| int property = metaObject()->indexOfProperty("lastVariant"); |
| QVERIFY(property != -1); |
| QVariant variant("foo bar asdf"); |
| channel.d_func()->publisher->setProperty(this, property, QJsonValue::fromVariant(variant)); |
| QCOMPARE(m_lastVariant, variant); |
| } |
| { |
| int property = metaObject()->indexOfProperty("lastJsonValue"); |
| QVERIFY(property != -1); |
| QJsonValue value("asdf asdf"); |
| channel.d_func()->publisher->setProperty(this, property, value); |
| QCOMPARE(m_lastJsonValue, value); |
| } |
| { |
| int property = metaObject()->indexOfProperty("lastJsonArray"); |
| QVERIFY(property != -1); |
| QJsonArray array; |
| array << QJsonValue(-123); |
| array << QJsonValue(-42); |
| channel.d_func()->publisher->setProperty(this, property, array); |
| QCOMPARE(m_lastJsonArray, array); |
| } |
| { |
| int property = metaObject()->indexOfProperty("lastJsonObject"); |
| QVERIFY(property != -1); |
| QJsonObject object; |
| object["foo"] = QJsonValue(-123); |
| object["bar"] = QJsonValue(-4.2); |
| channel.d_func()->publisher->setProperty(this, property, object); |
| QCOMPARE(m_lastJsonObject, object); |
| } |
| } |
| |
| void TestWebChannel::testInvokeMethodOverloadResolution() |
| { |
| QWebChannel channel; |
| TestObject testObject; |
| TestObject exportedObject; |
| channel.registerObject("test", &exportedObject); |
| channel.connectTo(m_dummyTransport); |
| |
| QVariant result; |
| QMetaObjectPublisher *publisher = channel.d_func()->publisher; |
| |
| { |
| result = publisher->invokeMethod(&testObject, "overload", { 41.0 }); |
| QVERIFY(result.userType() == QMetaType::Double); |
| QCOMPARE(result.toDouble(), 42.0); |
| } |
| { |
| // In JavaScript, there's only 'double', so this should always invoke the 'double' overload |
| result = publisher->invokeMethod(&testObject, "overload", { 41 }); |
| QVERIFY(result.userType() == QMetaType::Double); |
| QCOMPARE(result.toDouble(), 42); |
| } |
| { |
| QJsonObject wrappedObject { {"id", "test"} }; |
| result = publisher->invokeMethod(&testObject, "overload", { wrappedObject }); |
| QCOMPARE(result.value<TestObject*>(), &exportedObject); |
| } |
| { |
| result = publisher->invokeMethod(&testObject, "overload", { "hello world" }); |
| QCOMPARE(result.toString(), QStringLiteral("HELLO WORLD")); |
| } |
| { |
| result = publisher->invokeMethod(&testObject, "overload", { "the answer is ", 41 }); |
| QCOMPARE(result.toString(), QStringLiteral("THE ANSWER IS 42")); |
| } |
| { |
| QJsonArray args; |
| args.append(QJsonArray { "foobar", 42 }); |
| result = publisher->invokeMethod(&testObject, "overload", args); |
| QCOMPARE(result.toString(), QStringLiteral("42foobar")); |
| } |
| } |
| |
| void TestWebChannel::testDisconnect() |
| { |
| QWebChannel channel; |
| channel.connectTo(m_dummyTransport); |
| channel.disconnectFrom(m_dummyTransport); |
| m_dummyTransport->emitMessageReceived(QJsonObject()); |
| } |
| |
| void TestWebChannel::testWrapRegisteredObject() |
| { |
| QWebChannel channel; |
| TestObject obj; |
| obj.setObjectName("myTestObject"); |
| |
| channel.registerObject(obj.objectName(), &obj); |
| channel.connectTo(m_dummyTransport); |
| channel.d_func()->publisher->initializeClient(m_dummyTransport); |
| |
| QJsonObject objectInfo = channel.d_func()->publisher->wrapResult(QVariant::fromValue(&obj), m_dummyTransport).toObject(); |
| |
| QCOMPARE(2, objectInfo.length()); |
| QVERIFY(objectInfo.contains("id")); |
| QVERIFY(objectInfo.contains("__QObject*__")); |
| QVERIFY(objectInfo.value("__QObject*__").isBool() && objectInfo.value("__QObject*__").toBool()); |
| |
| QString returnedId = objectInfo.value("id").toString(); |
| |
| QCOMPARE(&obj, channel.d_func()->publisher->registeredObjects.value(obj.objectName())); |
| QCOMPARE(obj.objectName(), channel.d_func()->publisher->registeredObjectIds.value(&obj)); |
| QCOMPARE(obj.objectName(), returnedId); |
| } |
| |
| void TestWebChannel::testUnwrapObject() |
| { |
| QWebChannel channel; |
| |
| { |
| TestObject obj; |
| obj.setObjectName("testObject"); |
| channel.registerObject(obj.objectName(), &obj); |
| QObject *unwrapped = channel.d_func()->publisher->unwrapObject(obj.objectName()); |
| QCOMPARE(unwrapped, &obj); |
| } |
| { |
| TestObject obj; |
| QJsonObject objectInfo = channel.d_func()->publisher->wrapResult(QVariant::fromValue(&obj), m_dummyTransport).toObject(); |
| QObject *unwrapped = channel.d_func()->publisher->unwrapObject(objectInfo["id"].toString()); |
| QCOMPARE(unwrapped, &obj); |
| } |
| } |
| |
| void TestWebChannel::testTransportWrapObjectProperties() |
| { |
| QWebChannel channel; |
| |
| TestObject obj; |
| obj.setObjectName("testObject"); |
| channel.registerObject(obj.objectName(), &obj); |
| |
| DummyTransport *dummyTransport = new DummyTransport(this); |
| channel.connectTo(dummyTransport); |
| channel.d_func()->publisher->initializeClient(dummyTransport); |
| channel.d_func()->publisher->setClientIsIdle(true); |
| |
| QCOMPARE(channel.d_func()->publisher->transportedWrappedObjects.size(), 0); |
| |
| QObject objPropObject; |
| objPropObject.setObjectName("foobar"); |
| |
| obj.setObjectProperty(&objPropObject); |
| |
| channel.d_func()->publisher->sendPendingPropertyUpdates(); |
| |
| QCOMPARE(channel.d_func()->publisher->wrappedObjects.size(), 1); |
| const QString wrappedObjId = channel.d_func()->publisher->wrappedObjects.keys()[0]; |
| |
| QCOMPARE(channel.d_func()->publisher->transportedWrappedObjects.size(), 1); |
| QCOMPARE(channel.d_func()->publisher->transportedWrappedObjects.keys()[0], dummyTransport); |
| QCOMPARE(channel.d_func()->publisher->transportedWrappedObjects.values()[0], wrappedObjId); |
| } |
| |
| void TestWebChannel::testRemoveUnusedTransports() |
| { |
| QWebChannel channel; |
| DummyTransport *dummyTransport = new DummyTransport(this); |
| TestObject obj; |
| |
| channel.connectTo(dummyTransport); |
| channel.d_func()->publisher->initializeClient(dummyTransport); |
| |
| QMetaObjectPublisher *pub = channel.d_func()->publisher; |
| pub->wrapResult(QVariant::fromValue(&obj), dummyTransport); |
| |
| QCOMPARE(pub->wrappedObjects.size(), 1); |
| QCOMPARE(pub->registeredObjectIds.size(), 1); |
| |
| channel.disconnectFrom(dummyTransport); |
| delete dummyTransport; |
| |
| QCOMPARE(pub->wrappedObjects.size(), 0); |
| QCOMPARE(pub->registeredObjectIds.size(), 0); |
| } |
| |
| void TestWebChannel::testPassWrappedObjectBack() |
| { |
| QWebChannel channel; |
| TestObject registeredObj; |
| TestObject returnedObjMethod; |
| TestObject returnedObjProperty; |
| |
| registeredObj.setObjectName("registeredObject"); |
| |
| channel.registerObject(registeredObj.objectName(), ®isteredObj); |
| channel.connectTo(m_dummyTransport); |
| channel.d_func()->publisher->initializeClient(m_dummyTransport); |
| |
| QMetaObjectPublisher *pub = channel.d_func()->publisher; |
| QJsonObject returnedObjMethodInfo = pub->wrapResult(QVariant::fromValue(&returnedObjMethod), m_dummyTransport).toObject(); |
| QJsonObject returnedObjPropertyInfo = pub->wrapResult(QVariant::fromValue(&returnedObjProperty), m_dummyTransport).toObject(); |
| |
| QJsonArray argsMethod; |
| QJsonObject argMethod0; |
| argMethod0["id"] = returnedObjMethodInfo["id"]; |
| argsMethod << argMethod0; |
| QJsonObject argProperty; |
| argProperty["id"] = returnedObjPropertyInfo["id"]; |
| |
| pub->invokeMethod(®isteredObj, "setReturnedObject", argsMethod); |
| QCOMPARE(registeredObj.mReturnedObject, &returnedObjMethod); |
| pub->setProperty(®isteredObj, registeredObj.metaObject()->indexOfProperty("returnedObject"), argProperty); |
| QCOMPARE(registeredObj.mReturnedObject, &returnedObjProperty); |
| } |
| |
| void TestWebChannel::testWrapValues() |
| { |
| QWebChannel channel; |
| channel.connectTo(m_dummyTransport); |
| |
| { |
| QVariant variant = QVariant::fromValue(TestObject::Asdf); |
| QJsonValue value = channel.d_func()->publisher->wrapResult(variant, m_dummyTransport); |
| QVERIFY(value.isDouble()); |
| QCOMPARE(value.toInt(), (int) TestObject::Asdf); |
| } |
| { |
| TestObject::TestFlags flags = TestObject::FirstFlag | TestObject::SecondFlag; |
| QVariant variant = QVariant::fromValue(flags); |
| QJsonValue value = channel.d_func()->publisher->wrapResult(variant, m_dummyTransport); |
| QVERIFY(value.isDouble()); |
| QCOMPARE(value.toInt(), (int) flags); |
| } |
| { |
| QVector<int> vec{1, 2, 3}; |
| QVariant variant = QVariant::fromValue(vec); |
| QJsonValue value = channel.d_func()->publisher->wrapResult(variant, m_dummyTransport); |
| QVERIFY(value.isArray()); |
| QCOMPARE(value.toArray(), QJsonArray({1, 2, 3})); |
| } |
| } |
| |
| void TestWebChannel::testWrapObjectWithMultipleTransports() |
| { |
| QWebChannel channel; |
| QMetaObjectPublisher *pub = channel.d_func()->publisher; |
| |
| DummyTransport *dummyTransport = new DummyTransport(this); |
| DummyTransport *dummyTransport2 = new DummyTransport(this); |
| |
| TestObject obj; |
| |
| pub->wrapResult(QVariant::fromValue(&obj), dummyTransport); |
| pub->wrapResult(QVariant::fromValue(&obj), dummyTransport2); |
| |
| QCOMPARE(pub->transportedWrappedObjects.count(), 2); |
| } |
| |
| void TestWebChannel::testJsonToVariant() |
| { |
| QWebChannel channel; |
| channel.connectTo(m_dummyTransport); |
| |
| { |
| QVariant variant = QVariant::fromValue(TestObject::Asdf); |
| QVariant convertedValue = channel.d_func()->publisher->toVariant(static_cast<int>(TestObject::Asdf), variant.userType()); |
| QCOMPARE(convertedValue, variant); |
| } |
| { |
| TestObject::TestFlags flags = TestObject::FirstFlag | TestObject::SecondFlag; |
| QVariant variant = QVariant::fromValue(flags); |
| QVariant convertedValue = channel.d_func()->publisher->toVariant(static_cast<int>(flags), variant.userType()); |
| QCOMPARE(convertedValue, variant); |
| } |
| } |
| |
| void TestWebChannel::testInfiniteRecursion() |
| { |
| QWebChannel channel; |
| TestObject obj; |
| obj.setObjectProperty(&obj); |
| obj.setObjectName("myTestObject"); |
| |
| channel.connectTo(m_dummyTransport); |
| channel.d_func()->publisher->initializeClient(m_dummyTransport); |
| |
| QJsonObject objectInfo = channel.d_func()->publisher->wrapResult(QVariant::fromValue(&obj), m_dummyTransport).toObject(); |
| } |
| |
| void TestWebChannel::testAsyncObject() |
| { |
| QSKIP("This test is broken. See QTBUG-80729"); |
| |
| QWebChannel channel; |
| channel.connectTo(m_dummyTransport); |
| |
| QThread thread; |
| thread.start(); |
| |
| TestObject obj; |
| obj.moveToThread(&thread); |
| |
| QJsonArray args; |
| args.append(QJsonValue("message")); |
| |
| { |
| QSignalSpy spy(&obj, &TestObject::propChanged); |
| channel.d_func()->publisher->invokeMethod(&obj, "setProp", args); |
| QTRY_COMPARE(spy.count(), 1); |
| QCOMPARE(spy.at(0).at(0).toString(), args.at(0).toString()); |
| } |
| |
| channel.registerObject("myObj", &obj); |
| channel.d_func()->publisher->initializeClient(m_dummyTransport); |
| |
| QJsonObject connectMessage; |
| connectMessage["type"] = 7; |
| connectMessage["object"] = "myObj"; |
| connectMessage["signal"] = obj.metaObject()->indexOfSignal("replay()"); |
| channel.d_func()->publisher->handleMessage(connectMessage, m_dummyTransport); |
| |
| { |
| QSignalSpy spy(&obj, &TestObject::replay); |
| QMetaObject::invokeMethod(&obj, "fire"); |
| QTRY_COMPARE(spy.count(), 1); |
| channel.deregisterObject(&obj); |
| QMetaObject::invokeMethod(&obj, "fire"); |
| QTRY_COMPARE(spy.count(), 2); |
| } |
| |
| thread.quit(); |
| thread.wait(); |
| } |
| |
| class FunctionWrapper : public QObject |
| { |
| Q_OBJECT |
| std::function<void()> m_fun; |
| public: |
| FunctionWrapper(std::function<void()> fun) : m_fun(std::move(fun)) {} |
| public slots: |
| void invoke() |
| { |
| m_fun(); |
| } |
| }; |
| |
| void TestWebChannel::testDeletionDuringMethodInvocation_data() |
| { |
| QTest::addColumn<bool>("deleteChannel"); |
| QTest::addColumn<bool>("deleteTransport"); |
| QTest::newRow("delete neither") << false << false; |
| QTest::newRow("delete channel") << true << false; |
| QTest::newRow("delete transport") << false << true; |
| QTest::newRow("delete both") << true << true; |
| } |
| |
| void TestWebChannel::testDeletionDuringMethodInvocation() |
| { |
| QFETCH(bool, deleteChannel); |
| QFETCH(bool, deleteTransport); |
| |
| QScopedPointer<QWebChannel> channel(new QWebChannel); |
| QScopedPointer<DummyTransport> transport(new DummyTransport(nullptr)); |
| FunctionWrapper deleter([&](){ |
| if (deleteChannel) |
| channel.reset(); |
| if (deleteTransport) |
| transport.reset(); |
| }); |
| channel->registerObject("deleter", &deleter); |
| channel->connectTo(transport.data()); |
| |
| transport->emitMessageReceived({ |
| {"type", TypeInvokeMethod}, |
| {"object", "deleter"}, |
| {"method", deleter.metaObject()->indexOfMethod("invoke()")}, |
| {"id", 42} |
| }); |
| |
| QCOMPARE(deleteChannel, !channel); |
| QCOMPARE(deleteTransport, !transport); |
| if (!deleteTransport) |
| QCOMPARE(transport->messagesSent().size(), deleteChannel ? 0 : 1); |
| } |
| |
| static QHash<QString, QObject*> createObjects(QObject *parent) |
| { |
| const int num = 100; |
| QHash<QString, QObject*> objects; |
| objects.reserve(num); |
| for (int i = 0; i < num; ++i) { |
| objects[QStringLiteral("obj%1").arg(i)] = new BenchObject(parent); |
| } |
| return objects; |
| } |
| |
| void TestWebChannel::benchClassInfo() |
| { |
| QWebChannel channel; |
| channel.connectTo(m_dummyTransport); |
| |
| QObject parent; |
| const QHash<QString, QObject*> objects = createObjects(&parent); |
| |
| QBENCHMARK { |
| foreach (const QObject *object, objects) { |
| channel.d_func()->publisher->classInfoForObject(object, m_dummyTransport); |
| } |
| } |
| } |
| |
| void TestWebChannel::benchInitializeClients() |
| { |
| QWebChannel channel; |
| channel.connectTo(m_dummyTransport); |
| |
| QObject parent; |
| channel.registerObjects(createObjects(&parent)); |
| |
| QMetaObjectPublisher *publisher = channel.d_func()->publisher; |
| QBENCHMARK { |
| publisher->initializeClient(m_dummyTransport); |
| |
| publisher->propertyUpdatesInitialized = false; |
| publisher->signalToPropertyMap.clear(); |
| publisher->signalHandler.clear(); |
| } |
| } |
| |
| void TestWebChannel::benchPropertyUpdates() |
| { |
| QWebChannel channel; |
| channel.connectTo(m_dummyTransport); |
| |
| QObject parent; |
| const QHash<QString, QObject*> objects = createObjects(&parent); |
| QVector<BenchObject*> objectList; |
| objectList.reserve(objects.size()); |
| foreach (QObject *obj, objects) { |
| objectList << qobject_cast<BenchObject*>(obj); |
| } |
| |
| channel.registerObjects(objects); |
| channel.d_func()->publisher->initializeClient(m_dummyTransport); |
| |
| QBENCHMARK { |
| foreach (BenchObject *obj, objectList) { |
| obj->change(); |
| } |
| |
| channel.d_func()->publisher->clientIsIdle = true; |
| channel.d_func()->publisher->sendPendingPropertyUpdates(); |
| } |
| } |
| |
| void TestWebChannel::benchRegisterObjects() |
| { |
| QWebChannel channel; |
| channel.connectTo(m_dummyTransport); |
| |
| QObject parent; |
| const QHash<QString, QObject*> objects = createObjects(&parent); |
| |
| QBENCHMARK { |
| channel.registerObjects(objects); |
| } |
| } |
| |
| void TestWebChannel::benchRemoveTransport() |
| { |
| QWebChannel channel; |
| QList<DummyTransport*> dummyTransports; |
| for (int i = 500; i > 0; i--) |
| dummyTransports.append(new DummyTransport(this)); |
| |
| QList<QSharedPointer<TestObject>> objs; |
| QMetaObjectPublisher *pub = channel.d_func()->publisher; |
| |
| foreach (DummyTransport *transport, dummyTransports) { |
| channel.connectTo(transport); |
| channel.d_func()->publisher->initializeClient(transport); |
| |
| /* 30 objects per transport */ |
| for (int i = 30; i > 0; i--) { |
| QSharedPointer<TestObject> obj = QSharedPointer<TestObject>::create(); |
| objs.append(obj); |
| pub->wrapResult(QVariant::fromValue(obj.data()), transport); |
| } |
| } |
| |
| QBENCHMARK_ONCE { |
| for (auto transport : dummyTransports) |
| pub->transportRemoved(transport); |
| } |
| |
| qDeleteAll(dummyTransports); |
| } |
| |
| #ifdef WEBCHANNEL_TESTS_CAN_USE_JS_ENGINE |
| |
| class SubclassedTestObject : public TestObject |
| { |
| Q_OBJECT |
| Q_PROPERTY(QString bar READ bar WRITE setBar NOTIFY theBarHasChanged) |
| public: |
| void setBar(const QString &newBar); |
| signals: |
| void theBarHasChanged(); |
| }; |
| |
| void SubclassedTestObject::setBar(const QString &newBar) |
| { |
| if (!newBar.isNull()) |
| emit theBarHasChanged(); |
| } |
| |
| class TestSubclassedFunctor { |
| public: |
| TestSubclassedFunctor(TestJSEngine *engine) |
| : m_engine(engine) |
| { |
| } |
| |
| void operator()() { |
| QCOMPARE(m_engine->logger()->errorCount(), 0); |
| } |
| |
| private: |
| TestJSEngine *m_engine; |
| }; |
| #endif // WEBCHANNEL_TESTS_CAN_USE_JS_ENGINE |
| |
| void TestWebChannel::qtbug46548_overriddenProperties() |
| { |
| #ifndef WEBCHANNEL_TESTS_CAN_USE_JS_ENGINE |
| QSKIP("A JS engine is required for this test to make sense."); |
| #else |
| SubclassedTestObject obj; |
| obj.setObjectName("subclassedTestObject"); |
| |
| QWebChannel webChannel; |
| webChannel.registerObject(obj.objectName(), &obj); |
| TestJSEngine engine; |
| webChannel.connectTo(engine.transport()); |
| QSignalSpy spy(&engine, &TestJSEngine::channelSetupReady); |
| connect(&engine, &TestJSEngine::channelSetupReady, TestSubclassedFunctor(&engine)); |
| engine.initWebChannelJS(); |
| if (!spy.count()) |
| spy.wait(); |
| QCOMPARE(spy.count(), 1); |
| QJSValue subclassedTestObject = engine.evaluate("channel.objects[\"subclassedTestObject\"]"); |
| QVERIFY(subclassedTestObject.isObject()); |
| |
| #endif // WEBCHANNEL_TESTS_CAN_USE_JS_ENGINE |
| } |
| |
| void TestWebChannel::qtbug62388_wrapObjectMultipleTransports() |
| { |
| QWebChannel channel; |
| TestObject obj; |
| |
| auto initTransport = [&channel](QWebChannelAbstractTransport *transport) { |
| channel.connectTo(transport); |
| channel.d_func()->publisher->initializeClient(transport); |
| }; |
| initTransport(m_dummyTransport); |
| |
| auto queryObjectInfo = [&channel](QObject *obj, QWebChannelAbstractTransport *transport) { |
| return channel.d_func()->publisher->wrapResult(QVariant::fromValue(obj), transport).toObject(); |
| }; |
| |
| auto verifyObjectInfo = [&obj](const QJsonObject &objectInfo) { |
| |
| QCOMPARE(objectInfo.length(), 3); |
| QVERIFY(objectInfo.contains("id")); |
| QVERIFY(objectInfo.contains("__QObject*__")); |
| QVERIFY(objectInfo.contains("data")); |
| QVERIFY(objectInfo.value("__QObject*__").isBool() && objectInfo.value("__QObject*__").toBool()); |
| |
| const auto propIndex = obj.metaObject()->indexOfProperty("prop"); |
| const auto prop = objectInfo["data"].toObject()["properties"].toArray()[propIndex].toArray()[3].toString(); |
| QCOMPARE(prop, obj.prop()); |
| }; |
| |
| const auto objectInfo = queryObjectInfo(&obj, m_dummyTransport); |
| verifyObjectInfo(objectInfo); |
| |
| const auto id = objectInfo.value("id").toString(); |
| |
| QCOMPARE(channel.d_func()->publisher->unwrapObject(id), &obj); |
| |
| DummyTransport transport; |
| initTransport(&transport); |
| QCOMPARE(queryObjectInfo(&obj, &transport), objectInfo); |
| |
| obj.setProp("asdf"); |
| |
| const auto objectInfo2 = queryObjectInfo(&obj, m_dummyTransport); |
| QVERIFY(objectInfo2 != objectInfo); |
| verifyObjectInfo(objectInfo2); |
| |
| DummyTransport transport2; |
| initTransport(&transport2); |
| QCOMPARE(queryObjectInfo(&obj, &transport2), objectInfo2); |
| |
| // don't crash when the transports are destroyed |
| } |
| |
| QTEST_MAIN(TestWebChannel) |
| |
| #include "tst_webchannel.moc" |