diff options
-rw-r--r-- | .qmake.conf | 2 | ||||
-rw-r--r-- | examples/webchannel/shared/qwebchannel.js | 49 | ||||
-rw-r--r-- | src/webchannel/doc/src/javascript.qdoc | 48 | ||||
-rw-r--r-- | src/webchannel/qmetaobjectpublisher.cpp | 230 | ||||
-rw-r--r-- | src/webchannel/qmetaobjectpublisher_p.h | 53 | ||||
-rw-r--r-- | tests/auto/qml/testobject.cpp | 27 | ||||
-rw-r--r-- | tests/auto/qml/testobject.h | 15 | ||||
-rw-r--r-- | tests/auto/qml/tst_webchannel.qml | 118 | ||||
-rw-r--r-- | tests/auto/webchannel/tst_webchannel.cpp | 260 | ||||
-rw-r--r-- | tests/auto/webchannel/tst_webchannel.h | 15 |
10 files changed, 666 insertions, 151 deletions
diff --git a/.qmake.conf b/.qmake.conf index 7b49e2c..879fb0f 100644 --- a/.qmake.conf +++ b/.qmake.conf @@ -1,4 +1,4 @@ load(qt_build_config) CONFIG += warning_clean -MODULE_VERSION = 5.13.0 +MODULE_VERSION = 5.14.0 diff --git a/examples/webchannel/shared/qwebchannel.js b/examples/webchannel/shared/qwebchannel.js index 5b047c2..fca45d9 100644 --- a/examples/webchannel/shared/qwebchannel.js +++ b/examples/webchannel/shared/qwebchannel.js @@ -197,10 +197,16 @@ function QObject(name, data, webChannel) } return ret; } - if (!response - || !response["__QObject*__"] - || response.id === undefined) { + if (!(response instanceof Object)) return response; + + if (!response["__QObject*__"] + || response.id === undefined) { + var jObj = {}; + for (var propName in response) { + jObj[propName] = object.unwrapQObject(response[propName]); + } + return jObj; } var objectId = response.id; @@ -255,9 +261,16 @@ function QObject(name, data, webChannel) object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; object.__objectSignals__[signalIndex].push(callback); - if (!isPropertyNotifySignal && signalName !== "destroyed") { - // only required for "pure" signals, handled separately for properties in propertyUpdate - // also note that we always get notified about the destroyed signal + // only required for "pure" signals, handled separately for properties in propertyUpdate + if (isPropertyNotifySignal) + return; + + // also note that we always get notified about the destroyed signal + if (signalName === "destroyed" || signalName === "destroyed()" || signalName === "destroyed(QObject*)") + return; + + // and otherwise we only need to be connected only once + if (object.__objectSignals__[signalIndex].length == 1) { webChannel.exec({ type: QWebChannelMessageTypes.connectToSignal, object: object.__id__, @@ -307,7 +320,7 @@ function QObject(name, data, webChannel) // update property cache for (var propertyIndex in propertyMap) { var propertyValue = propertyMap[propertyIndex]; - object.__propertyCache__[propertyIndex] = propertyValue; + object.__propertyCache__[propertyIndex] = this.unwrapQObject(propertyValue); } for (var signalName in signals) { @@ -326,9 +339,14 @@ function QObject(name, data, webChannel) { var methodName = methodData[0]; var methodIdx = methodData[1]; + + // Fully specified methods are invoked by id, others by name for host-side overload resolution + var invokedMethod = methodName[methodName.length - 1] === ')' ? methodIdx : methodName + object[methodName] = function() { var args = []; var callback; + var errCallback; for (var i = 0; i < arguments.length; ++i) { var argument = arguments[i]; if (typeof argument === "function") @@ -341,10 +359,21 @@ function QObject(name, data, webChannel) args.push(argument); } + var result; + // during test, webChannel.exec synchronously calls the callback + // therefore, the promise must be constucted before calling + // webChannel.exec to ensure the callback is set up + if (!callback && (typeof(Promise) === 'function')) { + result = new Promise(function(resolve, reject) { + callback = resolve; + errCallback = reject; + }); + } + webChannel.exec({ "type": QWebChannelMessageTypes.invokeMethod, "object": object.__id__, - "method": methodIdx, + "method": invokedMethod, "args": args }, function(response) { if (response !== undefined) { @@ -352,8 +381,12 @@ function QObject(name, data, webChannel) if (callback) { (callback)(result); } + } else if (errCallback) { + (errCallback)(); } }); + + return result; }; } diff --git a/src/webchannel/doc/src/javascript.qdoc b/src/webchannel/doc/src/javascript.qdoc index e643034..ef44250 100644 --- a/src/webchannel/doc/src/javascript.qdoc +++ b/src/webchannel/doc/src/javascript.qdoc @@ -97,4 +97,52 @@ new QWebChannel(yourTransport, function(channel) { console.log(foo.MyEnum.MyEnumerator); }); \endcode + + \section2 Overloaded methods and signals + + When you publish a \c QObject that has overloaded methods, QWebChannel will resolve + method invocations to the best match. Note that due to JavaScript's type system, there is only + a single 'number' type which maps best to a C++ 'double'. When overloads differ only in the type + of a number-like parameter, QWebChannel will always choose that overload which best matches the + JavaScript 'number' type. + When you connect to an overloaded signal, the QWebChannel client will by default only connect to + the first signal overload of that name. + Additionally, overloads of methods and signals can explicitly be requested by their complete + \c QMetaMethod signature. + Assume we have the following \c QObject subclass on the C++ side: + + \code + class Foo : public QObject + { + Q_OBJECT + slots: + void foo(int i); + void foo(double d); + void foo(const QString &str); + void foo(const QString &str, int i); + + signals: + void bar(int i); + void bar(const QString &str); + void bar(const QString &str, int i); + }; + \endcode + + Then you can interact with this class on the JavaScript side like this: + + \code + // methods + foo.foo(42); // will call the method named foo which best matches the JavaScript number parameter, i.e. foo(double d) + foo.foo("asdf"); // will call foo(const QString &str) + foo.foo("asdf", 42); // will call foo(const QString &str, int i) + foo["foo(int)"](42); // explicitly call foo(int i), *not* foo(double d) + foo["foo(QString)"]("asdf"); // explicitly call foo(const QString &str) + foo["foo(QString,int)"]("asdf", 42); // explicitly call foo(const QString &str, int i) + + // signals + foo.bar.connect(...); // connect to first signal named bar, i.e. bar(int i) + foo["bar(int)"].connect(...); // connect explicitly to bar(int i) + foo["bar(QString)"].connect(...); // connect explicitly to bar(const QString &str) + foo["bar(QString,int)"].connect(...); // connect explicitly to bar(const QString &str, int i) + \endcode */ diff --git a/src/webchannel/qmetaobjectpublisher.cpp b/src/webchannel/qmetaobjectpublisher.cpp index c9285b7..9f5e9cd 100644 --- a/src/webchannel/qmetaobjectpublisher.cpp +++ b/src/webchannel/qmetaobjectpublisher.cpp @@ -83,6 +83,65 @@ bool isQFlagsType(uint id) return mo->indexOfEnumerator(name.constData()) > -1; } +// Common scores for overload resolution +enum OverloadScore { + PerfectMatchScore = 0, + VariantScore = 1, + NumberBaseScore = 2, + GenericConversionScore = 100, + IncompatibleScore = 10000, +}; + +// Scores the conversion of a double to a number-like user type. Better matches +// for a JS 'number' get a lower score. +int doubleToNumberConversionScore(int userType) +{ + switch (userType) { + case QMetaType::Bool: + return NumberBaseScore + 7; + case QMetaType::Char: + case QMetaType::SChar: + case QMetaType::UChar: + return NumberBaseScore + 6; + case QMetaType::Short: + case QMetaType::UShort: + return NumberBaseScore + 5; + case QMetaType::Int: + case QMetaType::UInt: + return NumberBaseScore + 4; + case QMetaType::Long: + case QMetaType::ULong: + return NumberBaseScore + 3; + case QMetaType::LongLong: + case QMetaType::ULongLong: + return NumberBaseScore + 2; + case QMetaType::Float: + return NumberBaseScore + 1; + case QMetaType::Double: + return NumberBaseScore; + default: + break; + } + + if (QMetaType::typeFlags(userType) & QMetaType::IsEnumeration) + return doubleToNumberConversionScore(QMetaType::Int); + + return IncompatibleScore; +} + +// Keeps track of the badness of a QMetaMethod candidate for overload resolution +struct OverloadResolutionCandidate +{ + OverloadResolutionCandidate(const QMetaMethod &method = QMetaMethod(), int badness = PerfectMatchScore) + : method(method), badness(badness) + {} + + QMetaMethod method; + int badness; + + bool operator<(const OverloadResolutionCandidate &other) const { return badness < other.badness; } +}; + MessageType toType(const QJsonValue &value) { int i = value.toInt(-1); @@ -122,6 +181,8 @@ QJsonObject createResponse(const QJsonValue &id, const QJsonValue &data) const int PROPERTY_UPDATE_INTERVAL = 50; } +Q_DECLARE_TYPEINFO(OverloadResolutionCandidate, Q_MOVABLE_TYPE); + QMetaObjectPublisher::QMetaObjectPublisher(QWebChannel *webChannel) : QObject(webChannel) , webChannel(webChannel) @@ -196,19 +257,13 @@ QJsonObject QMetaObjectPublisher::classInfoForObject(const QObject *object, QWeb propertyInfo.append(wrapResult(prop.read(object), transport)); qtProperties.append(propertyInfo); } - for (int i = 0; i < metaObject->methodCount(); ++i) { - if (notifySignals.contains(i)) { - continue; - } - const QMetaMethod &method = metaObject->method(i); - //NOTE: this must be a string, otherwise it will be converted to '{}' in QML - const QString &name = QString::fromLatin1(method.name()); - // optimize: skip overloaded methods/signals or property getters, on the JS side we can only - // call one of them anyways - // TODO: basic support for overloaded signals, methods - if (identifiers.contains(name)) { - continue; - } + auto addMethod = [&qtSignals, &qtMethods, &identifiers](int i, const QMetaMethod &method, const QByteArray &rawName) { + //NOTE: the name must be a string, otherwise it will be converted to '{}' in QML + const auto name = QString::fromLatin1(rawName); + // only the first method gets called with its name directly + // others must be called by explicitly passing the method signature + if (identifiers.contains(name)) + return; identifiers << name; // send data as array to client with format: [name, index] QJsonArray data; @@ -219,6 +274,15 @@ QJsonObject QMetaObjectPublisher::classInfoForObject(const QObject *object, QWeb } else if (method.access() == QMetaMethod::Public) { qtMethods.append(data); } + }; + for (int i = 0; i < metaObject->methodCount(); ++i) { + if (notifySignals.contains(i)) { + continue; + } + const QMetaMethod &method = metaObject->method(i); + addMethod(i, method, method.name()); + // for overload resolution also pass full method signature + addMethod(i, method, method.methodSignature()); } for (int i = 0; i < metaObject->enumeratorCount(); ++i) { QMetaEnum enumerator = metaObject->enumerator(i); @@ -365,17 +429,15 @@ void QMetaObjectPublisher::sendPendingPropertyUpdates() } } -QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const int methodIndex, +QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const QMetaMethod &method, const QJsonArray &args) { - const QMetaMethod &method = object->metaObject()->method(methodIndex); - if (method.name() == QByteArrayLiteral("deleteLater")) { // invoke `deleteLater` on wrapped QObject indirectly deleteWrappedObject(object); return QJsonValue(); } else if (!method.isValid()) { - qWarning() << "Cannot invoke unknown method of index" << methodIndex << "on object" << object << '.'; + qWarning() << "Cannot invoke invalid method on object" << object << '.'; return QJsonValue(); } else if (method.access() != QMetaMethod::Public) { qWarning() << "Cannot invoke non-public method" << method.name() << "on object" << object << '.'; @@ -419,6 +481,55 @@ QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const int met return returnValue; } +QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const int methodIndex, + const QJsonArray &args) +{ + const QMetaMethod &method = object->metaObject()->method(methodIndex); + if (!method.isValid()) { + qWarning() << "Cannot invoke method of unknown index" << methodIndex << "on object" + << object << '.'; + return QJsonValue(); + } + return invokeMethod(object, method, args); +} + +QVariant QMetaObjectPublisher::invokeMethod(QObject *const object, const QByteArray &methodName, + const QJsonArray &args) +{ + QVector<OverloadResolutionCandidate> candidates; + + const QMetaObject *mo = object->metaObject(); + for (int i = 0; i < mo->methodCount(); ++i) { + QMetaMethod method = mo->method(i); + if (method.name() != methodName || method.parameterCount() != args.count() + || method.access() != QMetaMethod::Public + || (method.methodType() != QMetaMethod::Method + && method.methodType() != QMetaMethod::Slot) + || method.parameterCount() > 10) + { + // Not a candidate + continue; + } + + candidates.append({method, methodOverloadBadness(method, args)}); + } + + if (candidates.isEmpty()) { + qWarning() << "No candidates found for" << methodName << "with" << args.size() + << "arguments on object" << object << '.'; + return QJsonValue(); + } + + std::sort(candidates.begin(), candidates.end()); + + if (candidates.size() > 1 && candidates[0].badness == candidates[1].badness) { + qWarning().nospace() << "Ambiguous overloads for method " << methodName << ". Choosing " + << candidates.first().method.methodSignature(); + } + + return invokeMethod(object, candidates.first().method, args); +} + void QMetaObjectPublisher::setProperty(QObject *object, const int propertyIndex, const QJsonValue &value) { QMetaProperty property = object->metaObject()->property(propertyIndex); @@ -489,7 +600,7 @@ QObject *QMetaObjectPublisher::unwrapObject(const QString &objectId) const { if (!objectId.isEmpty()) { ObjectInfo objectInfo = wrappedObjects.value(objectId); - if (objectInfo.object && !objectInfo.classinfo.isEmpty()) + if (objectInfo.object) return objectInfo.object; QObject *object = registeredObjects.value(objectId); if (object) @@ -531,6 +642,57 @@ QVariant QMetaObjectPublisher::toVariant(const QJsonValue &value, int targetType return variant; } +int QMetaObjectPublisher::conversionScore(const QJsonValue &value, int targetType) const +{ + if (targetType == QMetaType::QJsonValue) { + return PerfectMatchScore; + } else if (targetType == QMetaType::QJsonArray) { + return value.isArray() ? PerfectMatchScore : IncompatibleScore; + } else if (targetType == QMetaType::QJsonObject) { + return value.isObject() ? PerfectMatchScore : IncompatibleScore; + } else if (QMetaType::typeFlags(targetType) & QMetaType::PointerToQObject) { + if (value.isNull()) + return PerfectMatchScore; + if (!value.isObject()) + return IncompatibleScore; + + QJsonObject object = value.toObject(); + if (object[KEY_ID].isUndefined()) + return IncompatibleScore; + + QObject *unwrappedObject = unwrapObject(object[KEY_ID].toString()); + return unwrappedObject != Q_NULLPTR ? PerfectMatchScore : IncompatibleScore; + } else if (targetType == QMetaType::QVariant) { + return VariantScore; + } + + // Check if this is a number conversion + if (value.isDouble()) { + int score = doubleToNumberConversionScore(targetType); + if (score != IncompatibleScore) { + return score; + } + } + + QVariant variant = value.toVariant(); + if (variant.userType() == targetType) { + return PerfectMatchScore; + } else if (variant.canConvert(targetType)) { + return GenericConversionScore; + } + + return IncompatibleScore; +} + +int QMetaObjectPublisher::methodOverloadBadness(const QMetaMethod &method, const QJsonArray &args) const +{ + int badness = PerfectMatchScore; + for (int i = 0; i < args.size(); ++i) { + badness += conversionScore(args[i], method.parameterType(i)); + } + return badness; +} + void QMetaObjectPublisher::transportRemoved(QWebChannelAbstractTransport *transport) { auto it = transportedWrappedObjects.find(transport); @@ -574,7 +736,7 @@ QJsonValue QMetaObjectPublisher::wrapResult(const QVariant &result, QWebChannelA classInfo = classInfoForObject(object, transport); - ObjectInfo oi(object, classInfo); + ObjectInfo oi(object); if (transport) { oi.transports.append(transport); } else { @@ -595,7 +757,7 @@ QJsonValue QMetaObjectPublisher::wrapResult(const QVariant &result, QWebChannelA wrappedObjects[id].transports.append(transport); transportedWrappedObjects.insert(transport, id); } - classInfo = wrappedObjects.value(id).classinfo; + classInfo = classInfoForObject(object, transport); } QJsonObject objectInfo; @@ -620,6 +782,9 @@ QJsonValue QMetaObjectPublisher::wrapResult(const QVariant &result, QWebChannelA } else if (result.canConvert<QVariantList>()) { // recurse and potentially wrap contents of the array return wrapList(result.toList(), transport); + } else if (result.canConvert<QVariantMap>()) { + // recurse and potentially wrap contents of the map + return wrapMap(result.toMap(), transport); } return QJsonValue::fromVariant(result); @@ -634,6 +799,15 @@ QJsonArray QMetaObjectPublisher::wrapList(const QVariantList &list, QWebChannelA return array; } +QJsonObject QMetaObjectPublisher::wrapMap(const QVariantMap &map, QWebChannelAbstractTransport *transport, const QString &parentObjectId) +{ + QJsonObject obj; + for (QVariantMap::const_iterator i = map.begin(); i != map.end(); i++) { + obj.insert(i.key(), wrapResult(i.value(), transport, parentObjectId)); + } + return obj; +} + void QMetaObjectPublisher::deleteWrappedObject(QObject *object) const { if (!wrappedObjects.contains(registeredObjectIds.value(object))) { @@ -700,10 +874,18 @@ void QMetaObjectPublisher::handleMessage(const QJsonObject &message, QWebChannel QPointer<QMetaObjectPublisher> publisherExists(this); QPointer<QWebChannelAbstractTransport> transportExists(transport); - QVariant result = - invokeMethod(object, - message.value(KEY_METHOD).toInt(-1), - message.value(KEY_ARGS).toArray()); + QJsonValue method = message.value(KEY_METHOD); + QVariant result; + + if (method.isString()) { + result = invokeMethod(object, + method.toString().toUtf8(), + message.value(KEY_ARGS).toArray()); + } else { + result = invokeMethod(object, + method.toInt(-1), + message.value(KEY_ARGS).toArray()); + } if (!publisherExists || !transportExists) return; transport->sendMessage(createResponse(message.value(KEY_ID), wrapResult(result, transport))); diff --git a/src/webchannel/qmetaobjectpublisher_p.h b/src/webchannel/qmetaobjectpublisher_p.h index d02a933..6030de2 100644 --- a/src/webchannel/qmetaobjectpublisher_p.h +++ b/src/webchannel/qmetaobjectpublisher_p.h @@ -148,6 +148,14 @@ public: void sendPendingPropertyUpdates(); /** + * Invoke the @p method on @p object with the arguments @p args. + * + * The return value of the method invocation is then serialized and a response message + * is returned. + */ + QVariant invokeMethod(QObject *const object, const QMetaMethod &method, const QJsonArray &args); + + /** * Invoke the method of index @p methodIndex on @p object with the arguments @p args. * * The return value of the method invocation is then serialized and a response message @@ -156,6 +164,16 @@ public: QVariant invokeMethod(QObject *const object, const int methodIndex, const QJsonArray &args); /** + * Invoke the method of name @p methodName on @p object with the arguments @p args. + * + * This method performs overload resolution on @p methodName. + * + * The return value of the method invocation is then serialized and a response message + * is returned. + */ + QVariant invokeMethod(QObject *const object, const QByteArray &methodName, const QJsonArray &args); + + /** * Set the value of property @p propertyIndex on @p object to @p value. */ void setProperty(QObject *object, const int propertyIndex, const QJsonValue &value); @@ -177,6 +195,26 @@ public: QVariant toVariant(const QJsonValue &value, int targetType) const; /** + * Assigns a score for the conversion from @p value to @p targetType. + * + * Scores can be compared to find the best match. The lower the score, the + * more preferable is the conversion. + * + * @sa invokeMethod, methodOverloadBadness + */ + int conversionScore(const QJsonValue &value, int targetType) const; + + /** + * Scores @p method against @p args. + * + * Scores can be compared to find the best match from a set of overloads. + * The lower the score, the more preferable is the method. + * + * @sa invokeMethod, conversionScore + */ + int methodOverloadBadness(const QMetaMethod &method, const QJsonArray &args) const; + + /** * Remove wrapped objects which last transport relation is with the passed transport object. */ void transportRemoved(QWebChannelAbstractTransport *transport); @@ -199,6 +237,14 @@ public: const QString &parentObjectId = QString()); /** + * Convert a variant map for consumption by the client. + * + * This properly handles QML values and also wraps the result if required. + */ + QJsonObject wrapMap(const QVariantMap &map, QWebChannelAbstractTransport *transport, + const QString &parentObjectId = QString()); + + /** * Invoke delete later on @p object. */ void deleteWrappedObject(QObject *object) const; @@ -248,15 +294,10 @@ private: // Groups individually wrapped objects with their class information and the transports that have access to it. struct ObjectInfo { - ObjectInfo() - : object(Q_NULLPTR) - {} - ObjectInfo(QObject *o, const QJsonObject &i) + ObjectInfo(QObject *o = nullptr) : object(o) - , classinfo(i) {} QObject *object; - QJsonObject classinfo; QVector<QWebChannelAbstractTransport*> transports; }; diff --git a/tests/auto/qml/testobject.cpp b/tests/auto/qml/testobject.cpp index 2be7773..ad302e7 100644 --- a/tests/auto/qml/testobject.cpp +++ b/tests/auto/qml/testobject.cpp @@ -33,13 +33,22 @@ QT_BEGIN_NAMESPACE TestObject::TestObject(QObject* parent) : QObject(parent) + , embeddedObject(new QObject(this)) { + embeddedObject->setObjectName("embedded"); } TestObject::~TestObject() { } +QVariantMap TestObject::objectMap() const +{ + QVariantMap map; + map.insert("subObject", QVariant::fromValue(embeddedObject)); + return map; +} + void TestObject::triggerSignals() { emit testSignalBool(true); @@ -50,4 +59,22 @@ void TestObject::triggerSignals() emit testSignalInt(0); } +int TestObject::testOverload(int i) +{ + emit testOverloadSignal(i); + return i + 1; +} + +QString TestObject::testOverload(const QString &str) +{ + emit testOverloadSignal(str); + return str.toUpper(); +} + +QString TestObject::testOverload(const QString &str, int i) +{ + emit testOverloadSignal(str, i); + return str.toUpper() + QString::number(i + 1); +} + QT_END_NAMESPACE diff --git a/tests/auto/qml/testobject.h b/tests/auto/qml/testobject.h index e025ea6..b9c5ecc 100644 --- a/tests/auto/qml/testobject.h +++ b/tests/auto/qml/testobject.h @@ -31,22 +31,37 @@ #define TESTOBJECT_H #include <QObject> +#include <QVariantMap> QT_BEGIN_NAMESPACE class TestObject : public QObject { Q_OBJECT + Q_PROPERTY(QVariantMap objectMap READ objectMap CONSTANT) public: explicit TestObject(QObject *parent = Q_NULLPTR); ~TestObject(); + QVariantMap objectMap() const; + public slots: void triggerSignals(); + int testOverload(int i); + QString testOverload(const QString &str); + QString testOverload(const QString &str, int i); + signals: void testSignalBool(bool testBool); void testSignalInt(int testInt); + + void testOverloadSignal(int i); + void testOverloadSignal(const QString &str); + void testOverloadSignal(const QString &str, int i); + +private: + QObject *embeddedObject; }; QT_END_NAMESPACE diff --git a/tests/auto/qml/tst_webchannel.qml b/tests/auto/qml/tst_webchannel.qml index 6da6c4b..ed1c4a1 100644 --- a/tests/auto/qml/tst_webchannel.qml +++ b/tests/auto/qml/tst_webchannel.qml @@ -71,6 +71,9 @@ TestCase { lastFactoryObj = component.createObject(myFactory, {objectName: id}); return lastFactoryObj; } + function switchObject() { + otherObject = myOtherObj; + } property var objectInProperty: QtObject { objectName: "foo" } @@ -84,6 +87,7 @@ TestCase { property var myProperty : 0 function myMethod(arg) { lastMethodArg = arg; + return myProperty; } signal mySignal(var arg1, var arg2) } @@ -235,6 +239,7 @@ TestCase { var testObjBeforeDeletion; var testObjAfterDeletion; var testObjId; + var testReturn; var channel = client.createChannel(function(channel) { channel.objects.myFactory.create("testObj", function(obj) { testObjId = obj.__id__; @@ -246,7 +251,9 @@ TestCase { testObjAfterDeletion = obj; }); obj.myProperty = 42; - obj.myMethod("foobar"); + obj.myMethod("foobar").then(function(result) { + testReturn = result; + }); }); }); client.awaitInit(); @@ -285,11 +292,12 @@ TestCase { // the server should eventually notify the client about the property update client.awaitPropertyUpdate(); - client.awaitIdle(); + // check that the Promise from myMethod was resolved + // must happen after waiting for something so the Promise callback + // can execute + compare(testReturn, 42); - // trigger a signal and ensure it gets transmitted - lastFactoryObj.mySignal("foobar", 42); - client.awaitSignal(); + client.awaitIdle(); // property should be wrapped compare(channel.objects.myFactory.objectInProperty.objectName, "foo"); @@ -297,9 +305,24 @@ TestCase { compare(channel.objects.myFactory.objects.length, 2); compare(channel.objects.myFactory.objects[0].objectName, "bar"); compare(channel.objects.myFactory.objects[1].objectName, "baz"); + // map property as well + compare(channel.objects.testObject.objectMap.subObject.objectName, + "embedded"); // also works with properties that reference other registered objects compare(channel.objects.myFactory.otherObject, channel.objects.myObj); + // change object property + channel.objects.myFactory.switchObject(); + client.awaitMessage(); + client.awaitResponse(); + client.awaitIdle(); + client.awaitPropertyUpdate(); + compare(channel.objects.myFactory.otherObject, channel.objects.myOtherObj); + + // trigger a signal and ensure it gets transmitted + lastFactoryObj.mySignal("foobar", 42); + client.awaitSignal(); + // deleteLater call msg = client.awaitMessage(); compare(msg.type, JSClient.QWebChannelMessageTypes.invokeMethod); @@ -448,4 +471,89 @@ TestCase { 0 ]); } + + function test_multiConnect() + { + var signalArgs = []; + function logSignalArgs(arg) { + signalArgs.push(arg); + } + var channel = client.createChannel(function(channel) { + var testObject = channel.objects.testObject; + testObject.testSignalInt.connect(logSignalArgs); + testObject.testSignalInt.connect(logSignalArgs); + testObject.triggerSignals(); + }); + client.awaitInit(); + + var msg = client.awaitMessage(); + compare(msg.type, JSClient.QWebChannelMessageTypes.connectToSignal); + compare(msg.object, "testObject"); + + msg = client.awaitMessage(); + compare(msg.type, JSClient.QWebChannelMessageTypes.invokeMethod); + client.awaitIdle(); + + compare(signalArgs, [42, 42, 1, 1, 0, 0]); + } + + function test_overloading() + { + var signalArgs_implicit = []; + var signalArgs_explicit1 = []; + var signalArgs_explicit2 = []; + var signalArgs_explicit3 = []; + function logSignalArgs(container) { + return function(...args) { + container.push(args); + }; + } + var returnValues = []; + function logReturnValue(value) { + returnValues.push(value); + } + var channel = client.createChannel(function(channel) { + var testObject = channel.objects.testObject; + testObject.testOverloadSignal.connect(logSignalArgs(signalArgs_implicit)); + testObject["testOverloadSignal(int)"].connect(logSignalArgs(signalArgs_explicit1)); + testObject["testOverloadSignal(QString)"].connect(logSignalArgs(signalArgs_explicit2)); + testObject["testOverloadSignal(QString,int)"].connect(logSignalArgs(signalArgs_explicit3)); + testObject.testOverload(99, logReturnValue); + testObject["testOverload(int)"](41, logReturnValue); + testObject["testOverload(QString)"]("hello world", logReturnValue); + testObject["testOverload(QString,int)"]("the answer is ", 41, logReturnValue); + }); + client.awaitInit(); + + function awaitMessage(type) + { + var msg = client.awaitMessage(); + compare(msg.type, type); + compare(msg.object, "testObject"); + } + + console.log("sig1"); + awaitMessage(JSClient.QWebChannelMessageTypes.connectToSignal); + console.log("sig2"); + awaitMessage(JSClient.QWebChannelMessageTypes.connectToSignal); + console.log("sig3"); + awaitMessage(JSClient.QWebChannelMessageTypes.connectToSignal); + + console.log("method1"); + awaitMessage(JSClient.QWebChannelMessageTypes.invokeMethod); + console.log("method2"); + awaitMessage(JSClient.QWebChannelMessageTypes.invokeMethod); + console.log("method3"); + awaitMessage(JSClient.QWebChannelMessageTypes.invokeMethod); + console.log("method4"); + awaitMessage(JSClient.QWebChannelMessageTypes.invokeMethod); + + client.awaitIdle(); + + compare(signalArgs_implicit, [[99], [41]]); + compare(signalArgs_explicit1, signalArgs_implicit); + compare(signalArgs_explicit2, [["hello world"]]); + compare(signalArgs_explicit3, [["the answer is ", 41]]); + compare(returnValues, [100, 42, "HELLO WORLD", "THE ANSWER IS 42"]); + } } diff --git a/tests/auto/webchannel/tst_webchannel.cpp b/tests/auto/webchannel/tst_webchannel.cpp index 9a0f575..4e622c7 100644 --- a/tests/auto/webchannel/tst_webchannel.cpp +++ b/tests/auto/webchannel/tst_webchannel.cpp @@ -275,6 +275,21 @@ void TestWebChannel::setJsonArray(const QJsonArray& 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; @@ -351,85 +366,42 @@ void TestWebChannel::testInfoForObject() 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 - QJsonArray expected; - { - QJsonArray method; - method.append(QStringLiteral("deleteLater")); - method.append(obj.metaObject()->indexOfMethod("deleteLater()")); - expected.append(method); - } - { - QJsonArray method; - method.append(QStringLiteral("slot1")); - method.append(obj.metaObject()->indexOfMethod("slot1()")); - expected.append(method); - } - { - QJsonArray method; - method.append(QStringLiteral("slot2")); - method.append(obj.metaObject()->indexOfMethod("slot2(QString)")); - expected.append(method); - } - { - QJsonArray method; - method.append(QStringLiteral("setReturnedObject")); - method.append(obj.metaObject()->indexOfMethod("setReturnedObject(TestObject*)")); - expected.append(method); - } - { - QJsonArray method; - method.append(QStringLiteral("setObjectProperty")); - method.append(obj.metaObject()->indexOfMethod("setObjectProperty(QObject*)")); - expected.append(method); - } - { - QJsonArray method; - method.append(QStringLiteral("setProp")); - method.append(obj.metaObject()->indexOfMethod("setProp(QString)")); - expected.append(method); - } - { - QJsonArray method; - method.append(QStringLiteral("fire")); - method.append(obj.metaObject()->indexOfMethod("fire()")); - expected.append(method); - } - { - QJsonArray method; - method.append(QStringLiteral("method1")); - method.append(obj.metaObject()->indexOfMethod("method1()")); - expected.append(method); - } + 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 - QJsonArray expected; - { - QJsonArray signal; - signal.append(QStringLiteral("destroyed")); - signal.append(obj.metaObject()->indexOfMethod("destroyed(QObject*)")); - expected.append(signal); - } - { - QJsonArray signal; - signal.append(QStringLiteral("sig1")); - signal.append(obj.metaObject()->indexOfMethod("sig1()")); - expected.append(signal); - } - { - QJsonArray signal; - signal.append(QStringLiteral("sig2")); - signal.append(obj.metaObject()->indexOfMethod("sig2(QString)")); - expected.append(signal); - } - { - QJsonArray signal; - signal.append(QStringLiteral("replay")); - signal.append(obj.metaObject()->indexOfMethod("replay()")); - expected.append(signal); - } + 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); } @@ -537,9 +509,7 @@ void TestWebChannel::testInvokeMethodConversion() args.append(QJsonValue(1000)); { - int method = metaObject()->indexOfMethod("setInt(int)"); - QVERIFY(method != -1); - channel.d_func()->publisher->invokeMethod(this, method, args); + channel.d_func()->publisher->invokeMethod(this, "setInt", args); QCOMPARE(m_lastInt, args.at(0).toInt()); int getterMethod = metaObject()->indexOfMethod("readInt()"); QVERIFY(getterMethod != -1); @@ -547,11 +517,9 @@ void TestWebChannel::testInvokeMethodConversion() QCOMPARE(retVal, args.at(0).toVariant()); } { - int method = metaObject()->indexOfMethod("setBool(bool)"); - QVERIFY(method != -1); QJsonArray args; args.append(QJsonValue(!m_lastBool)); - channel.d_func()->publisher->invokeMethod(this, method, args); + channel.d_func()->publisher->invokeMethod(this, "setBool", args); QCOMPARE(m_lastBool, args.at(0).toBool()); int getterMethod = metaObject()->indexOfMethod("readBool()"); QVERIFY(getterMethod != -1); @@ -559,9 +527,7 @@ void TestWebChannel::testInvokeMethodConversion() QCOMPARE(retVal, args.at(0).toVariant()); } { - int method = metaObject()->indexOfMethod("setDouble(double)"); - QVERIFY(method != -1); - channel.d_func()->publisher->invokeMethod(this, method, args); + channel.d_func()->publisher->invokeMethod(this, "setDouble", args); QCOMPARE(m_lastDouble, args.at(0).toDouble()); int getterMethod = metaObject()->indexOfMethod("readDouble()"); QVERIFY(getterMethod != -1); @@ -569,9 +535,7 @@ void TestWebChannel::testInvokeMethodConversion() QCOMPARE(retVal, args.at(0).toVariant()); } { - int method = metaObject()->indexOfMethod("setVariant(QVariant)"); - QVERIFY(method != -1); - channel.d_func()->publisher->invokeMethod(this, method, args); + channel.d_func()->publisher->invokeMethod(this, "setVariant", args); QCOMPARE(m_lastVariant, args.at(0).toVariant()); int getterMethod = metaObject()->indexOfMethod("readVariant()"); QVERIFY(getterMethod != -1); @@ -579,9 +543,7 @@ void TestWebChannel::testInvokeMethodConversion() QCOMPARE(retVal, args.at(0).toVariant()); } { - int method = metaObject()->indexOfMethod("setJsonValue(QJsonValue)"); - QVERIFY(method != -1); - channel.d_func()->publisher->invokeMethod(this, method, args); + channel.d_func()->publisher->invokeMethod(this, "setJsonValue", args); QCOMPARE(m_lastJsonValue, args.at(0)); int getterMethod = metaObject()->indexOfMethod("readJsonValue()"); QVERIFY(getterMethod != -1); @@ -589,13 +551,11 @@ void TestWebChannel::testInvokeMethodConversion() QCOMPARE(retVal, args.at(0).toVariant()); } { - int method = metaObject()->indexOfMethod("setJsonObject(QJsonObject)"); - QVERIFY(method != -1); QJsonObject object; object["foo"] = QJsonValue(123); object["bar"] = QJsonValue(4.2); args[0] = object; - channel.d_func()->publisher->invokeMethod(this, method, args); + channel.d_func()->publisher->invokeMethod(this, "setJsonObject", args); QCOMPARE(m_lastJsonObject, object); int getterMethod = metaObject()->indexOfMethod("readJsonObject()"); QVERIFY(getterMethod != -1); @@ -603,13 +563,11 @@ void TestWebChannel::testInvokeMethodConversion() QCOMPARE(retVal, QVariant::fromValue(object)); } { - int setterMethod = metaObject()->indexOfMethod("setJsonArray(QJsonArray)"); - QVERIFY(setterMethod != -1); QJsonArray array; array << QJsonValue(123); array << QJsonValue(4.2); args[0] = array; - channel.d_func()->publisher->invokeMethod(this, setterMethod, args); + channel.d_func()->publisher->invokeMethod(this, "setJsonArray", args); QCOMPARE(m_lastJsonArray, array); int getterMethod = metaObject()->indexOfMethod("readJsonArray()"); QVERIFY(getterMethod != -1); @@ -618,6 +576,35 @@ void TestWebChannel::testInvokeMethodConversion() } } +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; @@ -676,6 +663,49 @@ void TestWebChannel::testSetPropertyConversion() } } +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; @@ -773,7 +803,7 @@ void TestWebChannel::testPassWrappedObjectBack() QJsonObject argProperty; argProperty["id"] = returnedObjPropertyInfo["id"]; - pub->invokeMethod(®isteredObj, registeredObj.metaObject()->indexOfSlot("setReturnedObject(TestObject*)"), argsMethod); + pub->invokeMethod(®isteredObj, "setReturnedObject", argsMethod); QCOMPARE(registeredObj.mReturnedObject, &returnedObjMethod); pub->setProperty(®isteredObj, registeredObj.metaObject()->indexOfProperty("returnedObject"), argProperty); QCOMPARE(registeredObj.mReturnedObject, &returnedObjProperty); @@ -860,12 +890,9 @@ void TestWebChannel::testAsyncObject() QJsonArray args; args.append(QJsonValue("message")); - int method = obj.metaObject()->indexOfMethod("setProp(QString)"); - QVERIFY(method != -1); - { QSignalSpy spy(&obj, &TestObject::propChanged); - channel.d_func()->publisher->invokeMethod(&obj, method, args); + channel.d_func()->publisher->invokeMethod(&obj, "setProp", args); QTRY_COMPARE(spy.count(), 1); QCOMPARE(spy.at(0).at(0).toString(), args.at(0).toString()); } @@ -1129,13 +1156,22 @@ void TestWebChannel::qtbug62388_wrapObjectMultipleTransports() auto queryObjectInfo = [&channel](QObject *obj, QWebChannelAbstractTransport *transport) { return channel.d_func()->publisher->wrapResult(QVariant::fromValue(obj), transport).toObject(); }; - const auto objectInfo = queryObjectInfo(&obj, m_dummyTransport); - 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()); + 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(); @@ -1145,7 +1181,17 @@ void TestWebChannel::qtbug62388_wrapObjectMultipleTransports() initTransport(&transport); QCOMPARE(queryObjectInfo(&obj, &transport), objectInfo); - // don't crash when the transport is destroyed + 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) diff --git a/tests/auto/webchannel/tst_webchannel.h b/tests/auto/webchannel/tst_webchannel.h index 3d16f7b..8ca1cdd 100644 --- a/tests/auto/webchannel/tst_webchannel.h +++ b/tests/auto/webchannel/tst_webchannel.h @@ -136,6 +136,8 @@ signals: void returnedObjectChanged(); void propChanged(const QString&); void replay(); + void overloadSignal(int); + void overloadSignal(float); public slots: void slot1() {} @@ -156,6 +158,13 @@ public slots: void setProp(const QString&prop) {emit propChanged(mProp=prop);} void fire() {emit replay();} + double overload(double d) { return d + 1; } + int overload(int i) { return i * 2; } + QObject *overload(QObject *object) { return object; } + QString overload(const QString &str) { return str.toUpper(); } + QString overload(const QString &str, int i) { return str.toUpper() + QString::number(i + 1); } + QString overload(const QJsonArray &v) { return QString::number(v[1].toInt()) + v[0].toString(); } + protected slots: void slot3() {} @@ -293,6 +302,10 @@ public slots: QJsonArray readJsonArray() const; void setJsonArray(const QJsonArray &v); + int readOverload(int i); + QString readOverload(const QString &arg); + QString readOverload(const QString &arg, int i); + signals: void lastIntChanged(); void lastBoolChanged(); @@ -308,7 +321,9 @@ private slots: void testDeregisterObjectAtStart(); void testInfoForObject(); void testInvokeMethodConversion(); + void testFunctionOverloading(); void testSetPropertyConversion(); + void testInvokeMethodOverloadResolution(); void testDisconnect(); void testWrapRegisteredObject(); void testUnwrapObject(); |